一家公司正在改变它的经营方式,因此它的员工需求预计也会发生变化。
通过购买新机器,预计对非熟练劳动力的需求将减少,而对熟练和半熟练劳动力的需求将增加。此外,由于预计明年将出现的经济放缓,销售预期下降,预计将进一步减少所有类别的劳动力需求。
未来三年的劳动力需求预测如下:
Unskilled | Semi-skilled | Skilled | |
---|---|---|---|
Current Strength | 2000 | 1500 | 1000 |
Year 1 | 1000 | 1400 | 1000 |
Year 2 | 500 | 2000 | 1500 |
Year 3 | 0 | 2500 | 2000 |
公司需要在未来三年每年确定以下事项:
值得注意的是,劳动力每年都一定程度的自然流失。新员工入职后第一年的流失率相对较高,随后几年的流失率相对较低。预期的流失率如下:
Unskilled (%) | Semi-skilled (%) | Skilled (%) | |
---|---|---|---|
$< 1$ year of service | 25 | 20 | 10 |
$\geq 1$ year of service | 10 | 5 | 5 |
目前所有的员工都在公司工作了至少一年。
每年,可以从公司外部雇佣有限数量的每个级别的员工,具体如下:
Unskilled | Semi-skilled | Skilled |
---|---|---|
500 | 800 | 500 |
每年可以培训多达200名非技术工人,使他们成为半技术工人。每个工人培训花费400美元。
此外,也可以对半熟练工人进行培训,使他们成为熟练工人。然而,这一数字不能超过当前熟练劳动力的25%,而且培训成本为每个工人500美元。
最后,工人也会被降级。但是,50%被降级的员工将离开公司,在自然流失率的基础上增加员工流失。
每名被解雇的工人可领取离职补偿金,每名非熟练工人领取200元,每名半熟练或熟练工人领取500元。
该公司允许一年至多有150名冗余工人,但这将导致每年一名多出的雇员产生以下额外成本。
Unskilled | Semi-skilled | Skilled |
---|---|---|
1500 | 2000 | 3000 |
每个技能级别最多可分配50名员工从事兼职工作。一年每名雇员的费用如下:
Unskilled | Semi-skilled | Skilled |
---|---|---|
500 | 400 | 400 |
注意
半技术性员工的培训: 在$t$年受训的半技术工人不得超过最大的培训数量。
$$
\begin{equation}
\text{train}_{t,s_2,s_3} \leq 0.25*\text{available}_{t,s_3} \quad \forall t \in \text{Years}
\end{equation}
$$
冗员: 在$t$年的冗余员工数量不得超过最大的数量。
$$
\begin{equation}
\sum_{s \in \text{Skills}}{\text{excess}_{t,s}} \leq \text{max_overmanning} \quad \forall t \in \text{Years}
\end{equation}
$$
参考资料
import numpy as np
import pandas as pd
import pulp as lp
We define all the input data of the model.
# Parameters
years = [1, 2, 3]
skills = ['s1', 's2', 's3']
curr_workforce = {'s1': 2000, 's2': 1500, 's3': 1000}
demand = {
(1, 's1'): 1000,
(1, 's2'): 1400,
(1, 's3'): 1000,
(2, 's1'): 500,
(2, 's2'): 2000,
(2, 's3'): 1500,
(3, 's1'): 0,
(3, 's2'): 2500,
(3, 's3'): 2000
}
rookie_attrition = {'s1': 0.25, 's2': 0.20, 's3': 0.10}
veteran_attrition = {'s1': 0.10, 's2': 0.05, 's3': 0.05}
demoted_attrition = 0.50
max_hiring = {
(1, 's1'): 500,
(1, 's2'): 800,
(1, 's3'): 500,
(2, 's1'): 500,
(2, 's2'): 800,
(2, 's3'): 500,
(3, 's1'): 500,
(3, 's2'): 800,
(3, 's3'): 500
}
max_overmanning = 150
max_parttime = 50
parttime_cap = 0.50
max_train_unskilled = 200
max_train_semiskilled = 0.25
training_cost = {'s1': 400, 's2': 500}
layoff_cost = {'s1': 200, 's2': 500, 's3': 500}
parttime_cost = {'s1': 500, 's2': 400, 's3': 400}
overmanning_cost = {'s1': 1500, 's2': 2000, 's3': 3000}
manpower = lp.LpProblem(name='Manpower_planning',sense=lp.LpMinimize)
hire = lp.LpVariable.dicts(name="Hire",indexs=[(t,s) for t in years for s in skills],lowBound=0,cat='Integer')
part_time = lp.LpVariable.dicts(name="Part_time",indexs=[(t,s) for t in years for s in skills], lowBound=0,upBound=max_parttime,cat='Integer')
workforce = lp.LpVariable.dicts(name="Available",indexs=[(t,s) for t in years for s in skills], lowBound=0,cat="Integer")
layoff = lp.LpVariable.dicts(name="Layoff",indexs=[(t,s) for t in years for s in skills], lowBound=0,cat="Integer")
excess = lp.LpVariable.dicts(name="Overmanned",indexs=[(t,s) for t in years for s in skills], lowBound=0,cat="Integer")
train = lp.LpVariable.dicts(name="Train",indexs=[(t,s,s1) for t in years for s in skills for s1 in skills], lowBound=0,cat="Integer")
for year in years:
for skill in skills:
manpower += hire[year,skill]<=max_hiring[(year,skill)]
#0.1 Objective Function: Minimize layoffs
obj1 = lp.lpSum(layoff[t,s] for t in years for s in skills)
manpower += obj1
#1.1 & 1.2 Balance
for year in years:
for level in skills:
if year == 1:
manpower += workforce[year, level] == (1-veteran_attrition[level])*(curr_workforce[level])+ (1-rookie_attrition[level])*hire[year, level] + lp.lpSum((1- veteran_attrition[level])* train[year, level2, level]-train[year, level, level2] for level2 in skills if level2 < level)+ lp.lpSum((1- demoted_attrition)* train[year, level2, level] -train[year, level, level2] for level2 in skills if level2 > level)
- layoff[year, level]
else:
manpower += workforce[year, level] == (1-veteran_attrition[level])*(workforce[year-1, level])+ (1-rookie_attrition[level])*hire[year, level] + lp.lpSum((1- veteran_attrition[level])* train[year, level2, level]-train[year, level, level2] for level2 in skills if level2 < level)+ lp.lpSum((1- demoted_attrition)* train[year, level2, level] -train[year, level, level2] for level2 in skills if level2 > level)
- layoff[year, level]
由于能力限制,每年只有200名工人可以从非技术培训再培训为半技术培训。而且,没有人能在一年内从不熟练变成熟练。
#2.1 & 2.2 Unskilled training
for year in years:
manpower += train[year, 's1', 's2'] <= max_train_unskilled
manpower += train[year, 's1', 's3'] == 0
将半技能工人再培训为熟练工人的人数不得超过当时熟练劳动力的四分之一
#3. Semi-skilled training
for year in years:
manpower += train[year,'s2', 's3'] <= max_train_semiskilled * workforce[year,'s3']
编制过剩的限制确保所有技能级别的人员在一年内的编制过剩总数不超过150人。
#4. Overmanning
for year in years:
manpower += lp.lpSum(excess[year, s] for s in skills) <= max_overmanning
确保每个级别和每年的工人数量等于所需的工人数量加上正式员工数量和兼职工作的工人数量。
#5. Demand
for year in years:
for level in skills:
manpower+=workforce[year, level] ==demand[year,level] + excess[year, level] + parttime_cap * part_time[year, level]
manpower.solve()
-1
The minimum number of layoffs is 841.80. The optimal policies to achieve this minimum number of layoffs are given below.
This plan determines the number of new workers to hire at each year of the planning horizon (rows) and each skill level (columns). For example, at year 2 we are going to hire 649.3 Semi-skilled workers.
rows = years.copy()
columns = skills.copy()
hire_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)
for year in years:
for level in skills:
if (abs(lp.value(hire[year, level]))) > 1e-6:
hire_plan.loc[year, level] = np.round(lp.value(hire[year, level]), 1)
hire_plan
s1 | s2 | s3 | |
---|---|---|---|
1 | 0.0 | 0.0 | 0.0 |
2 | 0.0 | 800.0 | 500.0 |
3 | 0.0 | 2374.3 | 500.0 |
This plan defines the number of workers to promote by training (or demote) at each year of the planning horizon. For example, in year 1 we are going to demote 168.4 skilled (s3) workers to the level of semi-skilled (s2).
rows = years.copy()
columns = ['{0} to {1}'.format(level1, level2) for level1 in skills for level2 in skills if level1 != level2]
train_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)
for year in years:
for level1 in skills:
for level2 in skills:
if level1 != level2:
col = '{0} to {1}'.format(level1, level2)
if (abs(lp.value(train[year, level1, level2])) > 1e-6):
train_plan.loc[year, col] = np.round(lp.value(train[year, level1, level2]), 1)
train_plan
s1 to s2 | s1 to s3 | s2 to s1 | s2 to s3 | s3 to s1 | s3 to s2 | |
---|---|---|---|---|---|---|
1 | 200.0 | 0.0 | 885.9 | 256.2 | 0.0 | 168.4 |
2 | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1281.6 |
3 | 200.0 | 0.0 | 0.0 | 1489.5 | 0.0 | 0.0 |
This plan determines the number of workers to layoff of each skill level at each year of the planning horizon. For example, we are going to layoff 232.5 Unskilled workers in year 3.
rows = years.copy()
columns = skills.copy()
layoff_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)
for year in years:
for level in skills:
if (abs(lp.value(layoff[year, level])) > 1e-6):
layoff_plan.loc[year, level] = np.round(lp.value(layoff[year, level]), 1)
layoff_plan
s1 | s2 | s3 | |
---|---|---|---|
1 | 0.0 | 0.0 | 0.0 |
2 | 0.0 | 0.0 | 0.0 |
3 | 0.0 | 0.0 | 0.0 |
This plan defines the number of part-time workers of each skill level working at each year of the planning horizon. For example, in year 1, we have 50 part-time skilled workers.
rows = years.copy()
columns = skills.copy()
parttime_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)
for year, level in part_time.keys():
if (abs(lp.value(part_time[year, level])) > 1e-6):
parttime_plan.loc[year, level] = np.round(lp.value(part_time[year, level]), 1)
parttime_plan
s1 | s2 | s3 | |
---|---|---|---|
1 | 50.0 | 50.0 | 50.0 |
2 | 1977.3 | 0.0 | -2715.8 |
3 | 2249.6 | 0.0 | 0.0 |
This plan determines the number of excess workers of each skill level working at each year of the planning horizon. For example, we have 150 Unskilled excess workers in year 3.
rows = years.copy()
columns = skills.copy()
excess_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)
for year, level in excess.keys():
if (abs(lp.value(excess[year, level])) > 1e-6):
excess_plan.loc[year, level] = np.round(lp.value(excess[year, level]), 1)
excess_plan
s1 | s2 | s3 | |
---|---|---|---|
1 | 1018.0 | -868.0 | 0.0 |
2 | 150.0 | 0.0 | 0.0 |
3 | 150.0 | 0.0 | 0.0 |
H. Paul Williams, Model Building in Mathematical Programming, fifth edition (Page 255-256, 354-356)